#include "HTTPRequest.h"

NahidaProject::HTTPRequest::Implement::TCPSocket::TCPSocket() noexcept : rawSocket(-1) {

}

NahidaProject::HTTPRequest::Implement::TCPSocket::~TCPSocket() noexcept {
	Close();
}

bool NahidaProject::HTTPRequest::Implement::TCPSocket::Connect(const std::string& host, std::uint16_t port) noexcept {
	rawSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (rawSocket == INVALID_SOCKET) {
		return false;
	}

	if (u_long mode = 1; ioctlsocket(rawSocket, FIONBIO, &mode) == SOCKET_ERROR) {
		return false;
	}

	addrinfo hints = {}, * res;
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;

	int result = getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &res);
	if (result != 0) {
		return false;
	}

	result = ::connect(rawSocket, res->ai_addr, res->ai_addrlen);
	freeaddrinfo(res);

	if (result == SOCKET_ERROR) {
		if (WSAGetLastError() == WSAEWOULDBLOCK) {
			return true;
		}
		return false;
	}

	return true;
}

bool NahidaProject::HTTPRequest::Implement::TCPSocket::Close() noexcept {
	if (rawSocket != INVALID_SOCKET && closesocket(rawSocket) != SOCKET_ERROR) {
		return true;
	}
	return false;
}

bool NahidaProject::HTTPRequest::Implement::TCPSocket::Send(const std::byte* data, std::size_t len) noexcept {
	if (::send(rawSocket, reinterpret_cast<const char*>(data), len, 0) != SOCKET_ERROR) {
		return true;
	}

	return false;
}

bool NahidaProject::HTTPRequest::Implement::TCPSocket::Receive(std::byte* data, std::size_t len) noexcept {
	int bytesReceived = recv(rawSocket, reinterpret_cast<char*>(data), len, 0);

	if (bytesReceived != SOCKET_ERROR && bytesReceived != 0) {
		return true;
	}
	else {
		return false;
	}
}

NahidaProject::HTTPRequest::AsyncHTTPRequest::AsyncHTTPRequest() noexcept : responseBuffer(4096), readyToReceive(false) {

}

std::string NahidaProject::HTTPRequest::AsyncHTTPRequest::GetHeader(const std::string& name) const noexcept {
	auto it = headers.find(name);
	if (it != headers.end()) {
		return it->second;
	}
	return "";
}

void NahidaProject::HTTPRequest::AsyncHTTPRequest::SetHeader(const std::string& name, const std::string& value) noexcept {
	headers[name] = value;
}

void NahidaProject::HTTPRequest::AsyncHTTPRequest::RemoveHeader(const std::string& name) noexcept {
	headers.erase(name);
}

void NahidaProject::HTTPRequest::AsyncHTTPRequest::ClearHeaders() noexcept {
	headers.clear();
}

void NahidaProject::HTTPRequest::AsyncHTTPRequest::Get(std::string_view url, const CallBack& callback) noexcept {
	std::string host;
	std::string path;

	if (!ParseURL(url, host, path)) {
		callback("Invalid URL");
		return;
	}

	if (!socket.Connect(host, 80)) {
		callback("Connection failed");
		return;
	}

	request = "GET " + path + " HTTP/1.1\r\n" + "Host: " + host + "\r\n";

	for (const auto& header : headers) {
		request += header.first + ": " + header.second + "\r\n";
	}

	request += "Connection: close\r\n\r\n";

	response.clear();

	readyToReceive = false;
	this->callback = callback;
}

void NahidaProject::HTTPRequest::AsyncHTTPRequest::Post(std::string_view url, CallBack callback, ContentType content_type, const std::string& data) noexcept {
	std::string host;
	std::string path;

	if (!ParseURL(url, host, path)) {
		callback("Invalid URL");
		return;
	}

	if (!socket.Connect(host, 80)) {
		callback("Connection failed");
		return;
	}

	std::string content_type_str;
	switch (content_type) {
	case ContentType::formUrlencoded:
		content_type_str = "application/x-www-form-urlencoded";
		break;
	case ContentType::json:
		content_type_str = "application/json";
		break;
	case ContentType::xml:
		content_type_str = "application/xml";
		break;
	case ContentType::text:
		content_type_str = "text/plain";
		break;
	default:
		content_type_str = "application/octet-stream";
		break;
	}

	std::string encoded_data = (content_type == ContentType::formUrlencoded) ? URLEncode(data) : data;

	request = "POST " + path + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Content-Type: " + content_type_str + "\r\n" + "Content-Length: " + std::to_string(encoded_data.length()) + "\r\n";

	for (const auto& header : headers) {
		request += header.first + ": " + header.second + "\r\n";
	}

	request += "Connection: close\r\n\r\n";
	request += encoded_data;

	response.clear();

	readyToReceive = false;
	this->callback = callback;
}

bool NahidaProject::HTTPRequest::AsyncHTTPRequest::Server() noexcept {
	if (readyToReceive) {
		if (socket.Receive(responseBuffer.data(), responseBuffer.size())) {
			response.append(reinterpret_cast<const char*>(responseBuffer.data()));
			responseBuffer.assign(responseBuffer.size(), std::byte{ 0 });

			packetTimeout = std::chrono::system_clock::now();
			packetTimeout += std::chrono::milliseconds(50);
		}

		if (std::chrono::system_clock::now() >= packetTimeout) {
			callback((response.empty()) ? "No data" : response);
			socket.Close();
			return true;
		}
	}
	else if (socket.Send(reinterpret_cast<const std::byte*>(request.c_str()), request.size())) {
		packetTimeout = std::chrono::system_clock::now();
		packetTimeout += std::chrono::milliseconds(5000);

		readyToReceive = true;
	}

	return false;
}

bool NahidaProject::HTTPRequest::AsyncHTTPRequest::ParseURL(std::string_view url, std::string& host, std::string& path) const noexcept {
	size_t protocol_pos = url.find("://");
	if (protocol_pos == std::string::npos) {
		return false;
	}

	protocol_pos += 3;
	size_t host_end = url.find('/', protocol_pos);
	if (host_end == std::string::npos) {
		host = url.substr(protocol_pos);
		path = "/";
	}
	else {
		host = url.substr(protocol_pos, host_end - protocol_pos);
		path = url.substr(host_end);
	}

	return true;
}

std::string NahidaProject::HTTPRequest::AsyncHTTPRequest::URLEncode(const std::string& data) const noexcept {
	std::string encoded_data;
	const std::string safe_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.~";
	for (char c : data) {
		if (std::isalnum(c) || safe_chars.find(c) != std::string::npos) {
			encoded_data.push_back(c);
		}
		else {
			encoded_data += "%" + ToHEX(static_cast<unsigned char>(c));
		}
	}
	return encoded_data;
}

std::string NahidaProject::HTTPRequest::AsyncHTTPRequest::ToHEX(unsigned char c) const noexcept {
	const char* hex_digits = "0123456789ABCDEF";
	std::string result;

	result.push_back(hex_digits[c >> 4]);
	result.push_back(hex_digits[c & 15]);

	return result;
}